package com.gframework.core.service.scheduled;

import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.redisson.executor.CronExpression;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;

import com.gframework.annotation.ThreadSafe;
import com.gframework.core.service.lock.DistributedLock;
/**
 * 动态管理定时任务操作工厂.<br>
 * 支持基于数据库或更多配置的定时任务创建，以及随时停止和重新设置规则。
 * 
 * <pre>
 * 1、本类采用懒启动模式，也就意味着如果没有创建任何定时任务，那么此类是不会创建线程占用资源的。
 * 2、本类占用的线程池大小是根据任务数量，执行周期等动态计算的 TODO 待实现
 * 3、此操作和spring其他的定时器互不影响
 * </pre>
 * <p>
 * 同时本类支持有分布式锁的分布式定时任务的执行，可以通过 {@link LockMode} 类来创建分布式锁策略。
 * 如果系统不支持有分布式锁操作，那么将会抛出异常。有关分布式锁的操作，可以参考 {@link DistributedLock}<br>
 * <strong>强烈建议，如果你的任务是需要进行分布式锁的定时任务，那么间隔时间最好在1分钟以上。</strong>
 * </p>
 * XXX 定时任务时间均为默认时区，暂不支持自定义时区
 * 
 * @since 1.0.0
 * @author Ghwolf
 * @see DistributedLock
 * @see LockMode
 */
@ThreadSafe
public class DynamicScheduledFactory {

	private static final Logger logger = LoggerFactory.getLogger(DynamicScheduledFactory.class);

	/**
	 * 定时器任务执行器，可以设置线程池大小，是一个多线程操作.
	 * <p>
	 * <strong>ThreadPoolTaskScheduler的线程池大小可以在运行时修改</strong>
	 * </p>
	 */
	private final ThreadPoolTaskScheduler taskPool;

	/**
	 * 存放所有动态管理的定时任务的map集合
	 */
	private final Map<String, ScheduledFuture<?>> futures = new ConcurrentHashMap<>();

	/**
	 * 分布式锁对象，如果为null，则所有分布式锁的操作逻辑都会无法实现并抛出异常，但是无分布式锁的操作逻辑依然可以正常执行
	 */
	@Nullable
	private final DistributedLock lock;
	
	/**
	 * 如果有提供DistributedLock接口对象，那么本类就支持分布式锁操作，否则不支持分布式锁.
	 * <p>
	 * 但是如果不支持分布式锁，会输出错误日志，但是不会影响程序正常执行。
	 * </p>
	 */
	public DynamicScheduledFactory(ObjectProvider<DistributedLock> dlock) {
		this.lock = dlock.getIfUnique();
		this.taskPool = new ThreadPoolTaskScheduler();
		this.taskPool.setDaemon(true);
		this.taskPool.setPoolSize(4);
		this.taskPool.initialize();
		logger.info("====> 已启用 DynamicScheduledFactory 动态管理定时任务工厂类——定时任务执行器并初始化完毕！");
	}

	/**
	 * 分布式锁模式.
	 * 用于指定定时任务在分布式环境下的执行逻辑。
	 * <p>
	 * 例如，在分布式环境下只要有一个被执行了就不在执行。
	 * 或者必须一个一个的去执行。
	 * </p>
	 * 
	 * @since 1.0.0
	 * @author Ghwolf
	 */
	public static class LockMode {

		/**
		 * 非秒级的cron表达式定时任务的OnlyOne模式对锁的最大租赁时间(毫秒)
		 */
		private static final int CRON_LEASE_TIME = 10 * 1000;
		/**
		 * 1分钟的毫秒数
		 */
		private static final int MINUTES_MILLISECONDS = 60 * 1000;

		/** 锁租赁时间 */
		long leaseTime;
		/** 时间单位 */
		TimeUnit unit;
		/** 分布式唯一key */
		String distributedKey;
		/** 是否是onlyOne模式 */
		boolean onlyOneMode;

		private LockMode(String distributedKey) {
			if (distributedKey == null) {
				throw new NullPointerException("distributedKey 不能为null！");
			}
			this.distributedKey = distributedKey;
		}

		private LockMode(String distributedKey, long leaseTime, TimeUnit unit) {
			this(distributedKey);
			this.leaseTime = leaseTime;
			this.unit = unit;
		}

		/**
		 * 采用类似synchronized的方式，一个一个的去执行定时任务。
		 * 但是锁有个最长租赁时间，如果超出了时间还没有执行完毕，则会自动释放锁。
		 * <p>
		 * 你需要根据你所执行的定时任务，来判断具体需要最大租赁多久。
		 * </p>
		 * 
		 * @param distributedKey 分布式锁唯一标识
		 * @param leaseTime 最多锁住多长时间
		 * @param unit 时间单位
		 * @throws NullPointerException 如果distributedKey为null
		 */
		public static LockMode synchronizedLeaseTime(String distributedKey, long leaseTime, TimeUnit unit) {
			return new LockMode(distributedKey, leaseTime, unit);
		}

		/**
		 * 采用类似synchronized的方式，一个一个的去执行定时任务。
		 * 但是锁有个最长租赁时间(秒)，如果超出了时间还没有执行完毕，则会自动释放锁。
		 * <p>
		 * 你需要根据你所执行的定时任务，来判断具体需要最大租赁多久。
		 * </p>
		 * 
		 * @param distributedKey 分布式锁唯一标识
		 * @param leaseSecondTime 最多锁住多长时间（秒）
		 * @throws NullPointerException 如果distributedKey为null
		 */
		public static LockMode synchronizedLeaseTime(String distributedKey, long leaseSecondTime) {
			return synchronizedLeaseTime(distributedKey, leaseSecondTime, TimeUnit.SECONDS);

		}

		/**
		 * 当所有的定时任务启动的时候，只有一个会去执行，其他的都会取消执行
		 * 
		 * @param distributedKey 分布式锁唯一标识
		 * @throws NullPointerException 如果distributedKey为null
		 */
		public static LockMode onlyOne(String distributedKey) {
			LockMode m = new LockMode(distributedKey);
			m.onlyOneMode = true;
			return m;
		}

		/**
		 * 是否是onlyOne模式
		 */
		final boolean isOnlyOne() {
			return this.onlyOneMode;
		}

		/**
		 * 设置onlyOne模式的租赁时间.
		 * 如果不是onlyOne模式，则执行此方法无任何效果
		 */
		void setOnlyOneLeaseTime(long millisecond) {
			if (isOnlyOne()) {
				if (millisecond < MINUTES_MILLISECONDS) {
					logger.warn("分布式定时任务的执行间隔不建议设置过短，建议最好在1分钟以上！");
				}
				this.leaseTime = millisecond;
				this.unit = TimeUnit.MILLISECONDS;
			}
		}

		/**
		 * 设置onlyOne模式的租赁时间.
		 * 如果不是onlyOne模式，则执行此方法无任何效果
		 */
		void setOnlyOneLeaseTime(String cron) {
			if (isOnlyOne()) {
				CronExpression ce = new CronExpression(cron);
				Date d1 = ce.getNextValidTimeAfter(new Date());
				Date d2 = ce.getNextValidTimeAfter(d1);
				if (d2.getTime() - d1.getTime() < MINUTES_MILLISECONDS) {
					logger.warn("分布式定时任务的执行间隔不建议设置过短，建议最好在1分钟以上！");
					this.leaseTime = 1000;
				} else {
					this.leaseTime = CRON_LEASE_TIME;
				}
				this.unit = TimeUnit.MILLISECONDS;
			}
		}

	}

	/**
	 * 取得一个支持分布式锁的执行定时任务操作对象。
	 */
	private Runnable proxyDistributed(Runnable run, LockMode lockMode) {
		if (this.lock == null) {
			throw new UnsupportedOperationException("DynamicScheduledFactory不支持分布式定时任务，因为没有任何一个分布式锁bean -> " + DistributedLock.class.getName());
		}
		return new DistributedRunnable(run, lockMode.distributedKey, lockMode.leaseTime, lockMode.unit,
				lockMode.isOnlyOne());
	}

	/**
	 * 支持分布式锁的Runnable代理对象
	 * 
	 * @author Ghwolf
	 */
	class DistributedRunnable implements Runnable {

		private final Runnable run;
		private final long leaseTime;
		private final TimeUnit unit;
		private final String distributedKey;
		private final boolean onlyOne;

		public DistributedRunnable(Runnable run, String distributedKey, long leaseTime, TimeUnit unit,
				boolean onlyOne) {
			this.run = run;
			this.leaseTime = leaseTime;
			this.unit = unit;
			this.distributedKey = distributedKey;
			this.onlyOne = onlyOne;
		}

		@Override
		public void run() {
			if (this.onlyOne) {
				// onlyOne 模式
				if (DynamicScheduledFactory.this.lock.tryLock(this.distributedKey, this.leaseTime, this.unit)) {
					this.run.run();
				}
			} else {
				// synchronizedLeaseTime 模式
				try {
					DynamicScheduledFactory.this.lock.lock(this.distributedKey, this.leaseTime, this.unit);
					this.run.run();
				} finally {
					DynamicScheduledFactory.this.lock.unlock(this.distributedKey);
				}
			}
		}

	}

	/**
	 * 是否支持分布式锁操作.
	 * 
	 * @return 如果支持返回true，否则返回false
	 */
	public boolean isSupportDistributedLock() {
		return this.lock != null;
	}

	/**
	 * 取得所有正在执行的定时器的key值集合
	 * 
	 * @return 返回Set集合，里面的key表示启动的定时器key值，如果没有则集合长度为0
	 */
	public Set<String> getStartedKeys() {
		return this.futures.keySet();
	}

	/**
	 * 停止一个定时任务.
	 * <p>
	 * 需要注意的是，停止定时任务只能够停止当前业务系统的定时任务，不能将所有的分布式/集群节点的定时任务都停止。
	 * </p>
	 * 
	 * @param key 要停止的任务key
	 * @return 如果此任务不存在，则返回false，否则即便已经停止或完成了，也会返回true。
	 */
	public boolean stopScheduled(String key) {
		ScheduledFuture<?> future = this.futures.remove(key);
		if (future == null) {
			if (logger.isInfoEnabled()) {
				logger.info("尝试停止key为：{}的定时任务，但是并不存在！", key);
			}
			return false;
		} else {
			boolean cancelSuccess = future.cancel(true);
			if (logger.isInfoEnabled()) {
				logger.info("尝试停止key为：{}的定时任务，{}", key, cancelSuccess ? "定时任务关闭成功！" : "定时任务已经关闭！");
			}
			return true;
		}
	}

	/**
	 * 创建或重新创建一个基于corn表达式的定时任务.
	 * 
	 * @param key 定时任务key值，可以通过此值进行定时任务的关闭
	 * @param task 执行操作
	 * @param corn corn表达式
	 * @return 如果不存在相同key值且运行着的定时任务，则返回true，表示这是一个新的，否则返回false，表示停止了旧的重新打开新的。
	 * @throws NullPointerException 如果任何一个参数为null，则抛出此异常
	 * @throws IllegalArgumentException 如果表达式错误，则抛出此异常
	 */
	public boolean setScheduled(String key, Runnable task, String corn) {
		this.assertNull(key, task, corn);
		// 先验证表达式是否正确
		CronTrigger trigger = new CronTrigger(corn);

		boolean flag = this.checkStop(key);
		this.futures.put(key, this.taskPool.schedule(task, trigger));
		if (logger.isInfoEnabled()) {
			logger.info("启动一个新的[corn:{}]定时任务：{}，{}", corn, key, this.getNewOrOldLogInfo(flag));
		}
		return flag;
	}

	/**
	 * 创建或重新创建一个具有指定执行间隔时间的任务，指定在一次执行完毕多久后执行第二次.
	 * <p>
	 * <strong>此任务创建后会立刻执行一次！</strong>
	 * </p>
	 * 
	 * @param key 定时任务key值，可以通过此值进行定时任务的关闭
	 * @param task 执行操作
	 * @param fixedDelay 任务结束后过多久再次执行(单位：毫秒)
	 * @return 如果不存在相同key值且运行着的定时任务，则返回true，表示这是一个新的，否则返回false，表示停止了旧的重新打开新的。
	 * @throws NullPointerException 如果任何一个参数为null，则抛出此异常
	 * @throws IllegalArgumentException {@code fixedDelay <= 0}
	 */
	public boolean setScheduledDelay(String key, Runnable task, long fixedDelay) {
		this.assertNull(key, task);
		if (fixedDelay <= 0) {
			throw new IllegalArgumentException("fixedDelay 值必须大于0（单位：毫秒），但是目前为：" + fixedDelay);
		}

		boolean flag = this.checkStop(key);
		this.futures.put(key, this.taskPool.scheduleWithFixedDelay(task, fixedDelay));
		if (logger.isInfoEnabled()) {
			logger.info("启动一个新的[delay:{}]定时任务：{}，{}", fixedDelay, key, this.getNewOrOldLogInfo(flag));
		}
		return flag;
	}

	/**
	 * 创建或重新创建一个具有指定执行间隔时间的任务，指定在一次执行完毕多久后执行第二次.
	 * 
	 * @param key 定时任务key值，可以通过此值进行定时任务的关闭
	 * @param task 执行操作
	 * @param initDelay 第一次执行前延迟时间（毫秒），如果小于0，按照0处理
	 * @param fixedDelay 任务结束后过多久再次执行(单位：毫秒)
	 * @return 如果不存在相同key值且运行着的定时任务，则返回true，表示这是一个新的，否则返回false，表示停止了旧的重新打开新的。
	 * @throws NullPointerException 如果任何一个参数为null，则抛出此异常
	 * @throws IllegalArgumentException {@code fixedDelay <= 0}
	 */
	public boolean setScheduledDelay(String key, Runnable task, long initDelay, long fixedDelay) {
		this.assertNull(key, task);
		if (fixedDelay <= 0) {
			throw new IllegalArgumentException("fixedDelay 值必须大于0（单位：毫秒），但是目前为：" + fixedDelay);
		}

		if (initDelay < 0) {
			initDelay = 0;
		}

		boolean flag = this.checkStop(key);
		this.futures.put(key, this.taskPool.scheduleWithFixedDelay(task, this.getStartTime(initDelay), fixedDelay));
		if (logger.isInfoEnabled()) {
			logger.info("启动一个新的[initDelay:{}][delay:{}]定时任务：{}，{}", initDelay, fixedDelay, key,
					this.getNewOrOldLogInfo(flag));
		}
		return flag;
	}

	/**
	 * 创建或重新创建一个按照时间间隔定时执行（不管前一次有没有执行完毕）的任务.
	 * <p>
	 * <strong>此任务创建后会立刻执行一次！</strong>
	 * </p>
	 * 
	 * @param key 定时任务key值，可以通过此值进行定时任务的关闭
	 * @param task 执行操作
	 * @param fixedRate 运行时间间隔(单位：毫秒)
	 * @return 如果不存在相同key值且运行着的定时任务，则返回true，表示这是一个新的，否则返回false，表示停止了旧的重新打开新的。
	 * @throws NullPointerException 如果任何一个参数为null，则抛出此异常
	 * @throws IllegalArgumentException {@code fixedDelay <= 0}
	 */
	public boolean setScheduledRate(String key, Runnable task, long fixedRate) {
		this.assertNull(key, task);
		if (fixedRate <= 0) {
			throw new IllegalArgumentException("fixedRate 值必须大于0（单位：毫秒），但是目前为：" + fixedRate);
		}

		boolean flag = this.checkStop(key);
		this.futures.put(key, this.taskPool.scheduleAtFixedRate(task, fixedRate));
		if (logger.isInfoEnabled()) {
			logger.info("启动一个新的[rate:{}]定时任务：{}，{}", fixedRate, key, this.getNewOrOldLogInfo(flag));
		}
		return flag;
	}

	/**
	 * 创建或重新创建一个按照时间间隔定时执行（不管前一次有没有执行完毕）的任务.
	 * 
	 * @param key 定时任务key值，可以通过此值进行定时任务的关闭
	 * @param task 执行操作
	 * @param initDelay 第一次执行前延迟时间（毫秒），如果小于0，按照0处理
	 * @param fixedRate 运行时间间隔(单位：毫秒)
	 * @return 如果不存在相同key值且运行着的定时任务，则返回true，表示这是一个新的，否则返回false，表示停止了旧的重新打开新的。
	 * @throws NullPointerException 如果任何一个参数为null，则抛出此异常
	 * @throws IllegalArgumentException {@code fixedDelay <= 0}
	 */
	public boolean setScheduledRate(String key, Runnable task, long initDelay, long fixedRate) {
		this.assertNull(key, task);
		if (fixedRate <= 0) {
			throw new IllegalArgumentException("fixedRate 值必须大于0（单位：毫秒），但是目前为：" + fixedRate);
		}

		if (initDelay < 0) {
			initDelay = 0;
		}

		boolean flag = this.checkStop(key);
		this.futures.put(key, this.taskPool.scheduleAtFixedRate(task, this.getStartTime(initDelay), fixedRate));
		if (logger.isInfoEnabled()) {
			logger.info("启动一个新的[initDelay:{}][rate:{}]定时任务：{}，{}", initDelay, fixedRate, key,
					this.getNewOrOldLogInfo(flag));
		}
		return flag;
	}

	/**
	 * 创建或重新创建一个基于corn表达式的支持分布式锁的定时任务.
	 * <p>
	 * 通过 {@link LockMode} 参数来指定具体采用哪种分布式锁策略
	 * </p>
	 * 
	 * @param key 定时任务key值，可以通过此值进行定时任务的关闭
	 * @param task 执行操作
	 * @param corn corn表达式
	 * @param lockMode 分布式锁模式 {@link LockMode}
	 * @return 如果不存在相同key值且运行着的定时任务，则返回true，表示这是一个新的，否则返回false，表示停止了旧的重新打开新的。
	 * @throws NullPointerException 如果任何一个参数为null，则抛出此异常
	 * @throws IllegalArgumentException 如果表达式错误，则抛出此异常
	 */
	public boolean setScheduled(String key, Runnable task, String corn, LockMode lockMode) {
		lockMode.setOnlyOneLeaseTime(corn);
		return this.setScheduled(key, this.proxyDistributed(task, lockMode), corn);
	}

	/**
	 * 创建或重新创建一个具有指定执行间隔时间的任务，指定在一次执行完毕多久后执行第二次.
	 * <p>
	 * <strong>此任务创建后会立刻执行一次！</strong>
	 * </p>
	 * 
	 * @param key 定时任务key值，可以通过此值进行定时任务的关闭
	 * @param task 执行操作
	 * @param fixedDelay 任务结束后过多久再次执行(单位：毫秒)
	 * @param lockMode 分布式锁模式 {@link LockMode}
	 * @return 如果不存在相同key值且运行着的定时任务，则返回true，表示这是一个新的，否则返回false，表示停止了旧的重新打开新的。
	 * @throws NullPointerException 如果任何一个参数为null，则抛出此异常
	 * @throws IllegalArgumentException {@code fixedDelay <= 0}
	 */
	public boolean setScheduledDelay(String key, Runnable task, long fixedDelay, LockMode lockMode) {
		lockMode.setOnlyOneLeaseTime(fixedDelay);
		return this.setScheduledDelay(key, this.proxyDistributed(task, lockMode), fixedDelay);
	}

	/**
	 * 创建或重新创建一个具有指定执行间隔时间的任务，指定在一次执行完毕多久后执行第二次.
	 * 
	 * @param key 定时任务key值，可以通过此值进行定时任务的关闭
	 * @param task 执行操作
	 * @param initDelay 第一次执行前延迟时间（毫秒），如果小于0，按照0处理
	 * @param fixedDelay 任务结束后过多久再次执行(单位：毫秒)
	 * @param lockMode 分布式锁模式 {@link LockMode}
	 * @return 如果不存在相同key值且运行着的定时任务，则返回true，表示这是一个新的，否则返回false，表示停止了旧的重新打开新的。
	 * @throws NullPointerException 如果任何一个参数为null，则抛出此异常
	 * @throws IllegalArgumentException {@code fixedDelay <= 0}
	 */
	public boolean setScheduledDelay(String key, Runnable task, long initDelay, long fixedDelay, LockMode lockMode) {
		lockMode.setOnlyOneLeaseTime(fixedDelay);
		return this.setScheduledDelay(key, this.proxyDistributed(task, lockMode), initDelay, fixedDelay);
	}

	/**
	 * 创建或重新创建一个按照时间间隔定时执行（不管前一次有没有执行完毕）的任务.
	 * <p>
	 * <strong>此任务创建后会立刻执行一次！</strong>
	 * </p>
	 * 
	 * @param key 定时任务key值，可以通过此值进行定时任务的关闭
	 * @param task 执行操作
	 * @param fixedRate 运行时间间隔(单位：毫秒)
	 * @param lockMode 分布式锁模式 {@link LockMode}
	 * @return 如果不存在相同key值且运行着的定时任务，则返回true，表示这是一个新的，否则返回false，表示停止了旧的重新打开新的。
	 * @throws NullPointerException 如果任何一个参数为null，则抛出此异常
	 * @throws IllegalArgumentException {@code fixedDelay <= 0}
	 */
	public boolean setScheduledRate(String key, Runnable task, long fixedRate, LockMode lockMode) {
		lockMode.setOnlyOneLeaseTime(fixedRate);
		return this.setScheduledRate(key, this.proxyDistributed(task, lockMode), fixedRate);
	}

	/**
	 * 创建或重新创建一个按照时间间隔定时执行（不管前一次有没有执行完毕）的任务.
	 * 
	 * @param key 定时任务key值，可以通过此值进行定时任务的关闭
	 * @param task 执行操作
	 * @param initDelay 第一次执行前延迟时间（毫秒），如果小于0，按照0处理
	 * @param fixedRate 运行时间间隔(单位：毫秒)
	 * @param lockMode 分布式锁模式 {@link LockMode}
	 * @return 如果不存在相同key值且运行着的定时任务，则返回true，表示这是一个新的，否则返回false，表示停止了旧的重新打开新的。
	 * @throws NullPointerException 如果任何一个参数为null，则抛出此异常
	 * @throws IllegalArgumentException {@code fixedDelay <= 0}
	 */
	public boolean setScheduledRate(String key, Runnable task, long initDelay, long fixedRate, LockMode lockMode) {
		lockMode.setOnlyOneLeaseTime(fixedRate);
		return this.setScheduledRate(key, this.proxyDistributed(task, lockMode), initDelay, fixedRate);
	}

	/**
	 * 取得描述这是一个新任务还是关了重新执行的一个就任务描述日志信息.
	 * 
	 * @param isNew 是否是新的
	 * @return 返回描述信息
	 */
	private final String getNewOrOldLogInfo(boolean isNew) {
		return isNew ? "这是一个全新的定时任务！" : "并关闭了旧的此id的定时任务！";
	}

	/**
	 * 根据初始延迟时间（毫秒）取得开始时间
	 * 
	 * @param initDelay 等待多久后开始执行第一次（毫秒）
	 * @return 返回Instant时刻对象
	 */
	private Date getStartTime(long initDelay) {
		return new Date(System.currentTimeMillis() + initDelay);
	}

	/**
	 * 检查一个任务是否已经停止.<br>
	 * 
	 * @param key 要检查的任务key
	 * @return 如果任务不存在或已经停止，则返回true，否则返回false并停止该任务
	 */
	private boolean checkStop(String key) {
		ScheduledFuture<?> currentFuture = this.futures.get(key);
		if (currentFuture == null) {
			return true;
		} else {
			boolean flag = currentFuture.isDone();
			if (!flag) {
				currentFuture.cancel(true);
			}
			return flag;
		}
	}

	/**
	 * 判断一个方法的全部参数是否存在null，如果存在，则抛出异常
	 * 
	 * @param args 要判断的参数
	 */
	private void assertNull(Object... args) {
		if (args == null || args.length == 0) return;
		for (Object arg : args) {
			if (arg == null) {
				throw new NullPointerException("参数不允许有null");
			}
		}
	}

}
